"""
Subversion backend for django-vcs.
"""
import pysvn
from datetime import datetime

from django.contrib.auth.models import User

from base import BackendRepository, BackendBranch, BackendTag, BackendCommit


class SVNRepository(BackendRepository):

    def __init__(self, model_obj):
        super(SVNRepository, self).__init__(self, model_obj=model_obj)
        # Store some SVN specific client variables
        self._client = pysvn.Client()
        self._imm = pysvn.depth.immediates
        self._head = pysvn.Revision(pysvn.opt_revision_kind.head)
        # Obtain the "real" branch/tag paths: user-defined or default.
        self.branch_path = self.model_obj.branch_path or '/branches'
        self.tag_path = self.model_obj.tag_path or '/tags'
   
    def refresh_tags(self):
        pass
        #from vcs.models import Tag
        #tags = self.repo.tags.all()
        #db_tags = set([SVNTag(repo=self.repo, name=x.name) for x in tags])
        #cur_tags = set(self.tags())
        #old_tags = set(self.old_tags())
        ## Add new tags
        #for tag in cur_tags.difference(db_tags):
        #    branch = tag.source_branch()
        #    ## Create new tag
        #    new_tag = Tag.objects.create(
        #        branch=branch.model_obj,
        #        name=tag.name,
        #        path=tag.path
        #    )
        ## Mark removed branches as removed
        #for name in old_branches.difference(db_branches):
        #    b = Branch.objects.get(name=name)
        #    b.removed = True
        #    b.save()

    def contents_of(self, path):
        """
        Returns two-tuple of files, folders contained at given repo path.
        """
        root_items = (x[0] for x in self._client.list(
            self.model_obj.url + path, 
            revision=self._head,
            depth=self._imm
        ))
        files = []
        folders = []
        for item in items:
            if item.kind == pysvn.node_kind.dir:
                folders.append(item)
            elif item.kind == pysvn.node_kind.file:
                files.append(item)
        return (files, folders)

    def branch_setup(self):
        """
        Determine what "format" this SVN repo is in and return an appropriate
        string:
        - 'master': entire repo in one branch
        - 'standard': can assume branches other than trunk get branch_path
            prefix, and can look in tag_path for tags.
        """
        # Setup
        root_files, root_folders = self.contents_of('/')
        branch_files, branch_folders = self.contents_of(self.branch_path)
        tag_files, tag_folders = self.contents_of(self.tag_path)
        # First, see if we need to make master branch due to top level
        # files (or files in branch_path or tag_path)
        if root_files or branch_files or tag_files:
            return 'master'
        # Next, see if we totally lack branch_path AND trunk, which also
        # means we need to shoehorn the repo into a master branch.
        if self.branch_path not in root_folders and 'trunk' not in root_folders:
            return 'master'
        # Otherwise, we must have at least branches and/or trunk, so we're
        # in standard mode.
        return 'standard'
 
    def new_commits(self):
        commits = None
        since = None
        if self.model_obj.commits.count():
            since = self.model_obj.commits.all().order_by('-identifier')[0]
        # Obtain list of commit entries since the given revision
        if since:
            base_revision = pysvn.Revision(pysvn.opt_revision_kind.number, 
                int(since.identifier))
            commits = client.log(
                self.model_obj.url, 
                revision_end=base_revision, 
                discover_changed_paths=True
            )
        else:
            commits = client.log(
                self.model_obj.url,
                discover_changed_paths=True
            )
        # pysvn.log(revision_end=N) will always return at least N.
        # Thus, we can return nothing if we only get zero (empty repo)
        # or one (nonempty repo with nothing new) item in the list.
        # However, must account for a newish repository with a single (not yet
        # seen by us) commit,
        if (not len(commits)) \
            or (len(commits) == 1 and (commits[0]['revision'].number != 1
                or self.model_obj.commits.count() == 1)):
            return []
        return [SVNCommit(repo=self, identifier=x['revision'].number)
            for x in commits]

    def db_branches(self):
        return [SVNBranch(repo=self, name=x.name) for x in
            self.model_obj.branches.all()]

    def branches(self):
        pass

    def old_branches(self):
        pass

    def tags(self):
        pass

    def old_tags(self):
        pass

    def is_empty(self):
        pass


class SVNBranch(BackendBranch):

    def path(self):
        """
        Take a branch name (single string, no slashes) and return its expected
        repository file path (e.g. /branches/foo). Really only matters for
        tacking on '/branches/' or the branch_path; the other scenarios can
        usually stay un-modified (i.e. /foo).
        """
        name = self.name.strip('/')
        if self.repo.branch_setup() == 'standard' and name != 'trunk':
            return '/' + self.repo.branch_path + '/' + name
        else:
            return '/' + name


class SVNTag(BackendTag):

    def source_branch(self):
        """
        Returns an SVNBranch object this tag appears to have come from, or
        None if no branch was found.
        """
        # discover first time tag existed
        # should be evidence of copy action from a branch
        # if that evidence is NOT clear...what then? ugh
        # if it is clear, try to get branch for that branch path
        # if can't find branch, more ugh (see 2 lines up)
        pass


class SVNCommit(BackendCommit):

    def create(self):
        new_commit = Commit.objects.create(
            identifier=unicode(commit['revision'].number),
            commit_timestamp=timestamp,
            author_timestamp=timestamp,
            committer=user,
            author=user,
            message=commit['message'],
            is_head=True
        )

    def parents(self):
        pass

    def update_file_modifications(self):
        for changed_path in self['changed_paths']:
            FileModification.objects.create(
                commit=self.model_obj,
                modification_type=changed_path['action'],
                filepath=changed_path['path']
            )






#
# Global variables
#

imm = pysvn.depth.immediates
# NOTE: 'client' may have to get worked back in if cxn params are needed?
# For now it's cleaner not to have to pass it around in all functions.
client = pysvn.Client()


#
# Subroutines/helpers
#

def _names(list_result):
    """
    Takes result of PySVN .list() call and returns actual repo path strings
    (instead of PySVN objects).
    """
    return [x[0].repos_path for x in list_result]

def _find_branches_and_tags(repo, revision):
    """
    Returns two-tuple of branch, tag names extant in repo at specified revision.
    """
    holder = {
        'branch_list': [],
        'tag_list': [],
    }
    # Obtain root-level folders
    folders = _names(client.list(repo.url, revision=revision, depth=imm))
    folders.remove(u'/') # get rid of root 'folder'
    # Obtain path by inspecting repo.*_path:
    # - empty string: use '/(tags|branches)'
    # - single '/': use all top level folders for branches, no tags at all
    # - other: use explicitly
    # NOTE: for some reason, client.list results in e.g. '//trunk', so keep
    # that in mind.
    for which in ['branch', 'tag']:
        path = getattr(repo, which+'_path')
        plural = (which == 'branch') and 'branches' or 'tags'
        # Root path only applies to branches.
        if path == '/':
            if which == 'branch':
                holder['branch_list'] = [x.lstrip('/') for x in folders]
            # Implicitly, tags are completely ignored here
        # Otherwise, we take the path or a default if empty.
        else:
            folder = '/' + (path or plural).lstrip('/')
            folder_path = repo.url + folder
            # Assume the given path exists
            try:
                names = _names(client.list(folder_path, depth=imm,
                    revision=revision))
                # Strip out prefix: pysvn names are absolute ('/branches/foo') 
                # but we just want the name, not the path.
                for name in names:
                    if name != folder: # Also remove branch folder itself
                        holder[which+'_list'].append(name.split('/')[-1])
            # And if it doesn't exist, just skip ahead (i.e. add no branches/
            # tags) but raise other ClientErrors in case its' a more serious one
            except pysvn.ClientError, e:
                s = str(e).lower()
                if "non-existent in that revision" not in s and \
                    "file not found" not in s:
                    raise e
            # Add in trunk if it exists
            if '//trunk' in folders and which == 'branch':
                holder['branch_list'].append('trunk')
    # Handle case where there is no '/branches' AND user has not specified
    # a custom branches path: just make single 'master' branch (like Git).
    # This is because we need at least one branch for everything to work.
    # Tags can still be empty as they are non-crucial.
    if not holder['branch_list']:
        holder['branch_list'] = ['master']
    return (holder['branch_list'], holder['tag_list'])


#
# Public methods
#

def refresh(repo):
    # Must import within function to avoid circular-import problems with models
    from vcs.models import Branch, Commit, FileModification
    # Need a list here, not a QuerySet, to avoid multiple queries
    active_branches = list(repo.active_branches.all())
    previous = None
    
    # See if there are any new commits in repo.
    # If not, nothing more will happen (for commit in []).
    # But, first handle special case of new, 100% empty repository and create
    # initial master branch from that.
    commits = _new_commits(repo)
    if not commits:
        # Get entire history to check if it's empty
        base = pysvn.Revision(pysvn.opt_revision_kind.number, 0)
        head = pysvn.Revision(pysvn.opt_revision_kind.head)
        all_commits = client.log(repo.url, revision_start=base,
            revision_end=head)
        # If empty
        if not all_commits:
            # Make master branch
            master = Branch.objects.create(repository=repo, name='master',
                path='/')

    # For each new commit:
    for commit in sorted(commits, key=lambda x:x['revision'].number):
        #
        # Update branches (add new ones, mark inactive ones as removed)
        #
        branches, tags = _find_branches_and_tags(repo, commit.revision)

        # Compare to current in-db branches and add anything new
        for branch in branches:
            if branch not in (x.name for x in active_branches):
                path = '/' + branch[:]
                # If this is the single-master-branch setup, make an empty path
                if branch == 'master' and len(branches) == 1:
                    path = '/'
                # Otherwise, add branch prefix to non-trunk branches
                elif branch != 'trunk':
                    path = '/branches/' + branch
                # Create branch, append to active branches
                new_branch = Branch.objects.create(
                    repository=repo,
                    name=branch,
                    path=path
                )
                active_branches.append(new_branch)

        # Branches in `active_branches` which are not in `branches` must have
        # been removed since the last time we updated, so mark them as such
        active_branch_names = [x.name for x in active_branches]
        for name in set(active_branch_names).difference(set(branches)):
            b = Branch.objects.get(name=name)
            b.removed = True
            b.save()

        #
        # Determine which branch(es) the commit is in
        # (TODO: handle tags too)
        #
        
        branches = []
        # Master branch setup just gets...the master branch.
        if active_branch_names == ['master']:
            branches = ['master']
        # Otherwise, examine path(s) to determine branch(es)
        else:
            for path in (x['path'] for x in commit['changed_paths']):
                parts = filter(None, path.split('/'))
                # Strip out branch prefix
                if parts and parts[0] == 'branches':
                    parts = parts[1:]
                # Below test will ignore commit paths that aren't in a branch
                # or in trunk, such as tags.
                if parts and parts[0] in active_branch_names:
                    branches.append(parts[0])

        #
        # Add the commit to those branch(es) (updating HEAD and etc)
        # TODO: >1 branch should imply some sort of merge, take this into acct
        #
        
        # Create commit object
        user = User.objects.get_or_create(username=commit['author'])[0]
        timestamp = datetime.fromtimestamp(commit['date'])
        new_commit = Commit.objects.create(
            identifier=unicode(commit['revision'].number),
            commit_timestamp=timestamp,
            author_timestamp=timestamp,
            committer=user,
            author=user,
            message=commit['message'],
            is_head=True
        )

        # Add to branches
        for branch in branches:
            new_commit.branches.add(repo.branches.get(name=branch))

        # Update parent
        if previous:
            new_commit.parents.add(previous)
        previous = new_commit
    
        # Update modified files
        for changed_path in commit['changed_paths']:
            FileModification.objects.create(
                commit=new_commit,
                modification_type=changed_path['action'],
                filepath=changed_path['path']
            )
